package com.leyou.search.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.leyou.common.pojo.PageResult;
import com.leyou.item.pojo.*;
import com.leyou.search.client.BrandClient;
import com.leyou.search.client.CategoryClient;
import com.leyou.search.client.GoodsClient;
import com.leyou.search.client.SpecificationClient;
import com.leyou.search.pojo.Goods;
import com.leyou.search.pojo.SearchRequest;
import com.leyou.search.pojo.SearchResult;
import com.leyou.search.repository.GoodsRepository;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.LongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

@Service
public class SearchService {

    @Autowired
    private CategoryClient categoryClient;
    @Autowired
    private BrandClient brandClient;
    @Autowired
    private GoodsClient goodsClient;
    @Autowired
    private SpecificationClient specificationClient;

    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Autowired
    private GoodsRepository goodsRepository;

    /**
     * 查询goods的分页结果集
     * @param request
     * @return
     */
    public SearchResult search(SearchRequest request) {
        if (StringUtils.isBlank(request.getKey())){
            return null;
        }
        //构造自定义查询建造器
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        //添加查询条件
        //QueryBuilder basicQuery = QueryBuilders.matchQuery("all", request.getKey()).operator(Operator.AND);
        BoolQueryBuilder boolQuery = buildBoolQueryBuilder(request);
        queryBuilder.withQuery(boolQuery);
        //添加分页条件,它是从零开始，所以要减1
        queryBuilder.withPageable(PageRequest.of(request.getPage()-1,request.getSize()));
        //设置过滤条件
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id","skus","subTitle"},null));

        //添加分类和品牌的聚合
        String categoryAggName = "categories";
        String brandAggName = "brands";
        queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));
        queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));

        //执行条件，返回结果集，并且强转成带有聚合结果集的集合
        AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>)(this.goodsRepository.search(queryBuilder.build()));

        //解析聚合结果集
        List<Map<String, Object>> categories = getCategoryAggResult(goodsPage.getAggregation(categoryAggName));
        List<Brand> brands = getBrandAggResult(goodsPage.getAggregation(brandAggName));
        //解析规格参数
        List<Map<String, Object>> specs = null;
        if (!CollectionUtils.isEmpty(categories) && categories.size() == 1){
             specs = getSpecAggResult((Long)(categories.get(0).get("id")), boolQuery);
        }

        return new SearchResult(goodsPage.getTotalElements(),goodsPage.getTotalPages(),goodsPage.getContent(),categories,brands,specs);
    }

    /**
     * 构建布尔查询
     * @param request
     * @return
     */
    private BoolQueryBuilder buildBoolQueryBuilder(SearchRequest request) {
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        //给布尔查询添加基本查询条件
        boolQueryBuilder.must(QueryBuilders.matchQuery("all",request.getKey()).operator(Operator.AND));
        //添加过滤条件
        //获取过滤请求
        Map<String, Object> filter = request.getFilter();

        for (Map.Entry<String, Object> entry : filter.entrySet()) {
            String key = entry.getKey();
            if (StringUtils.equals("分类",key)){
                key = "cid3";
            }else if (StringUtils.equals("品牌",key)){
                key = "brandId";
            }else{
                key = "specs."+key+".keyword";
            }
            boolQueryBuilder.filter(QueryBuilders.termQuery(key,entry.getValue()));
        }

        return boolQueryBuilder;
    }


    /**
     *根据查询条件聚合规格参数
     * @param id 分类cid3
     * @param basicQuery
     * @return
     */
    private List<Map<String, Object>> getSpecAggResult(Long id, QueryBuilder basicQuery) {
        //自定义查询构造器
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        //添加基本查询条件
        queryBuilder.withQuery(basicQuery);
        //查询要聚合的规格参数
        List<SpecParam> params = this.specificationClient.querySpecParams(null, id, null, true);
        //添加聚合条件
        params.forEach(param->{
            queryBuilder.addAggregation(AggregationBuilders.terms(param.getName()).field("specs."+param.getName()+".keyword"));
        });
        //过滤结果集条件
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{},null));
        //执行查询,获取聚合结果集
        AggregatedPage<Goods> goodsPage =(AggregatedPage<Goods>) this.goodsRepository.search(queryBuilder.build());
        //解析结果集
        List<Map<String, Object>> specs = new ArrayList<>();
        Map<String, Aggregation> stringAggregationMap = goodsPage.getAggregations().asMap();
        for (Map.Entry<String, Aggregation> entry : stringAggregationMap.entrySet()) {
            //初始化一个map，k-规格参数名，options-聚合的规格参数值
            Map<String, Object> map = new HashMap<>();
            //初始化一个options集合，收集桶中的key
            List<String> options = new ArrayList<>();
            map.put("k",entry.getKey());
            StringTerms terms = (StringTerms)entry.getValue();
            terms.getBuckets().forEach(bucket -> {
                options.add(bucket.getKeyAsString());
            });

            map.put("options",options);
            specs.add(map);
        }

        return specs;
    }

    /**
     * 解析品牌聚合的结果集
     * @param aggregation
     * @return
     */
    private List<Brand> getBrandAggResult(Aggregation aggregation) {
        //根据聚合的类型，强转
        LongTerms terms = (LongTerms)aggregation;
        //jdk8的强大功能，流转换，每个桶的key得出brand，返回即可,只有一句时，lambda直接安排，没有大括号和return
         return terms.getBuckets().stream().map(bucket ->
                 this.brandClient.queryBrandById(bucket.getKeyAsNumber().longValue()))
                 .collect(Collectors.toList());
    }

    /**
     * 解析分类聚合的结果集
     * @param aggregation
     * @return
     */
    private List<Map<String, Object>> getCategoryAggResult(Aggregation aggregation) {
        //强转聚合类型
        LongTerms terms = (LongTerms)aggregation;
        //获取桶的每个元素，根据key值获取map，返回即可
        return terms.getBuckets().stream().map(bucket -> {
            Map<String, Object> map = new HashMap<>();
            //获取分类id，也就是key值
            Long id = bucket.getKeyAsNumber().longValue();
            //这里的Client客户端是利用feign远程调用服务的方法，处理得到集合。看类型是否需要强转
            //根据id获取分类名称！
            List<String> names = this.categoryClient.queryNameByIds(Arrays.asList(id));
            map.put("id",id);
            map.put("name",names.get(0));
            return map;
        }).collect(Collectors.toList());

    }

    /**
     * 根据页面展示所需以及spu，构建goods
     * @param spu
     * @return
     * @throws IOException
     */
    public Goods buildGoods(Spu spu) throws IOException {

        Goods goods = new Goods();
        //根据分类id获取分类名称集合
        List<String> names = this.categoryClient.queryNameByIds(Arrays.asList(spu.getCid3(), spu.getCid2(), spu.getCid1()));
        //根据品牌id获取品牌
        Brand brand = this.brandClient.queryBrandById(spu.getBrandId());
        //根据spuId获取sku的集合
        List<Sku> skus = this.goodsClient.querySkusBySpuId(spu.getId());
        //初始化一个价格集合，收集所有sku的价格
        List<Long> prices = new ArrayList<>();
        //初始化一个sku集合，收集sku的必要字段信息
        List<Map<String, Object>> maps = new ArrayList<>();
        skus.forEach(sku -> {
            prices.add(sku.getPrice());
            //初始化一个map集合，收集一个sku对象有用的信息
            Map<String, Object> map = new HashMap<>();
            map.put("id", sku.getId());
            map.put("title", sku.getTitle());
            map.put("price", sku.getPrice());
            //图像是个集合，可能存在多张图片，并且用逗号隔开。因此这里将其用”，“切割并组成数组，默认用第一张图
            map.put("image", StringUtils.isNotBlank(sku.getImages()) ? StringUtils.split(sku.getImages(), ",")[0]:"");
            maps.add(map);

        });
        List<SpecParam> params = this.specificationClient.querySpecParams(null, spu.getCid3(), null, true);
        //根据spuId查询spuDetail
        SpuDetail spuDetail = this.goodsClient.querySpuDetailBySpuId(spu.getId());
        //将json格式的通用规格参数，反序列化，typereference
        Map<String, Object> genericSpecMap = MAPPER.readValue(spuDetail.getGenericSpec(), new TypeReference<Map<String, Object>>() {
        });
        //将json格式的特殊规格参数，反序列化
        Map<String, List<Object>> specialSpecMap = MAPPER.readValue(spuDetail.getSpecialSpec(), new TypeReference<Map<String, List<Object>>>() {
        });
        //初始化一个map集合，装有用信息
        Map<String, Object> spec = new HashMap<>();
        params.forEach(param -> {
            //判断是否为通用规格参数
            if (param.getGeneric()) {
                String value = genericSpecMap.get(param.getId().toString()).toString();
                //判断是否为数值类型，如果是数值类型，则返回一个区间
                if (param.getNumeric()) {
                    value = chooseSegment(value,param);
                }
                //装数据进去
                spec.put(param.getName(), value);
            }else {
                //特殊规格参数，没有区间判断，直接取值，id为转为String类型才能获得map的value值
                List<Object> value = specialSpecMap.get(param.getId().toString());
                //装数据进去
                spec.put(param.getName(), value);
            }

        });
        goods.setId(spu.getId());
        goods.setCid1(spu.getCid1());
        goods.setCid2(spu.getCid2());
        goods.setCid3(spu.getCid3());
        goods.setBrandId(spu.getBrandId());
        goods.setCreateTime(spu.getCreateTime());
        goods.setSubTitle(spu.getSubTitle());
        //设置all搜索字段，还需要分类名称、品牌名称，字符串空格隔开，是为了避免奇怪分词出现
        goods.setAll(spu.getTitle() + " " + StringUtils.join(names, " ") + " " + brand.getName());
        //获取spu下所有sku的价格
        goods.setPrice(prices);
        //获取spu下sku的部分有用信息，id、title、image、price并转化为json字符串
        goods.setSkus(MAPPER.writeValueAsString(maps));
        //设置规格参数，Map集合，根据不同维度key聚合value，{name：value}
        goods.setSpecs(spec);

        return goods;
    }

    /**
     * 返回数值区间
     * @param value
     * @param p
     * @return
     */
    private String chooseSegment(String value, SpecParam p) {
            double val = NumberUtils.toDouble(value);
            String result = "其它";
            // 保存数值段
            for (String segment : p.getSegments().split(",")) {
                String[] segs = segment.split("-");
                // 获取数值范围
                double begin = NumberUtils.toDouble(segs[0]);
                double end = Double.MAX_VALUE;
                if(segs.length == 2){
                    end = NumberUtils.toDouble(segs[1]);
                }
                // 判断是否在范围内
                if(val >= begin && val < end){
                    if(segs.length == 1){
                        result = segs[0] + p.getUnit() + "以上";
                    }else if(begin == 0){
                        result = segs[1] + p.getUnit() + "以下";
                    }else{
                        result = segment + p.getUnit();
                    }
                    break;
                }
            }
            return result;
        }


    public void save(Long id) throws IOException {
        Spu spu = this.goodsClient.querySpuById(id);
        Goods goods = this.buildGoods(spu);
        this.goodsRepository.save(goods);
    }

    public void delete(Long id) {
        this.goodsRepository.deleteById(id);
    }
}
